/*
 * This file is part of Annuaire.
 *
 * Annuaire is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Annuaire is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Annuaire.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.miage.annuaire;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * Cette classe modélise une structure de l'annuaire. Une structure est
 * caractérisée par son nom court et son nom complet. Une structure se compose
 * d'un leader, d'assistants leader, d'un contact, et de membres. Une structure
 * peut contenir des sous-structures, qui elles-mêmes peuvent contenir des
 * sous-structures.
 * @author guhumber
 */
public class Structure {

	/**
	 * Constructeur vide par défaut.
	 */
	public Structure() {
		// Rien.
	}

	/**
	 * Constructeur qui initialise des attributs.
	 * @param shortName Le nom abrégé (initiales par exemple) de la structure.
	 * @param completeName Le nom complet de la structure.
	 */
	public Structure(String shortName, String completeName) {
		this();
		this.shortName = shortName;
		this.completeName = completeName;
	}

	/**
	 * Constructeur qui initialise des attributs.
	 * @param key La clef de la structure.
	 * @param shortName Le nom abrégé (initiales par exemple) de la structure.
	 * @param completeName Le nom complet de la structure.
	 */
	public Structure(String key, String shortName, String completeName) {
		this(shortName, completeName);
		this.key = key;
	}

	

	/**
	 * Génère un hash de cet objet. Le hash est généré avec les attributs
	 * 'shortName' et 'completeName'.
	 * @return Le hash de cet objet.
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		
		result = prime * result
				+ ((this.key == null) ? 0 : this.key.hashCode());
		
		result = prime * result
				+ ((this.shortName == null) ? 0 : this.shortName.hashCode());
		
		result = prime * result + ((this.completeName == null) ? 0
				: this.completeName.hashCode());
		
		if (this.membres != null) {
			for (Membre membre : this.membres) {
				if (membre != null) {
					result = prime * result + ((membre.getKey() == null) ? 0
							: membre.getKey().hashCode());
				}
			}
		}
		
		result = prime * result + ((this.leader == null
				|| this.leader.getKey() == null) ? 0
						: this.leader.getKey().hashCode());
		
		result = prime * result + ((this.contact == null
				|| this.contact.getKey() == null) ? 0
						: this.contact.getKey().hashCode());
		
		result = prime * result + ((this.father == null
				|| this.father.getKey() == null) ? 0
						: this.father.getKey().hashCode());
		
		if (this.childs != null) {
			for (Structure child : this.childs) {
				if (child != null) {
					result = prime * result + ((child.getKey() == null) ? 0
							: child.getKey().hashCode());
				}
			}
		}
		
		return result;
	}

	/**
	 * Définit les critères d'égalité entre structures. Deux structure sont
	 * égales si elles ont le même nom court et le même nom complet.
	 * @param obj L'objet à comparer à cette structure.
	 * @return Vrai si 'obj' est un objet Structure et est égal à cette
	 * structure, faux sinon.
	 */
	@Override
	public boolean equals(Object obj) {
		
		if (this == obj) {
			return true;
		}
			
		if (obj == null) {
			return false;
		}
			
		if (!(obj instanceof Structure)) {
			return false;
		}
			
		Structure other = (Structure) obj;
		
		return this.hashCode() == other.hashCode();
	}

	/**
	 * Renvoie une représentation textuelle de cette structure, en fournissant
	 * le nom court et le nom long de cette structure.
	 * @return Une représentation textuelle de cette structure.
	 */
	@Override
	public String toString() {
		return "Structure{" + "shortName=" + this.shortName + "; completeName="
				+ this.completeName + '}';
	}

	/**
	 * Accesseur en lecture sur l'attribut 'key'.
	 * @return La clef de la structure.
	 */
	public String getKey() {
		return this.key;
	}

	/**
	 * Accesseur en écriture sur l'attribut 'key'.
	 * @param key La clef de la structure.
	 */
	public void setKey(String key) {
		this.key = key;
	}

	/**
	 * Accesseur en lecture sur l'attribut 'level'.
	 * @return Le niveau de profondeur de la structure.
	 */
	public int getLevel() {
		int level = 1;
		
		for (Structure s = this; s.getFather() != null; s = s.getFather()) {
			level++;
		}
		
		return level;
	}

	/**
	 * Accesseur en lecture sur l'attribut 'shortName'.
	 * @return Le nom abrégé (initiales par exemple) de la structure.
	 */
	public String getShortName() {
		return this.shortName;
	}

	/**
	 * Accesseur en écriture sur l'attribut 'shortName'.
	 * @param shortName Le nom abrégé (initiales par exemple) de la structure.
	 */
	public void setShortName(String shortName) {
		this.shortName = shortName;
	}

	/**
	 * Accesseur en lecture sur l'attribut 'completeName'.
	 * @return Le nom complet de la structure.
	 */
	public String getCompleteName() {
		return this.completeName;
	}

	/**
	 * Accesseur en écriture sur l'attribut 'completeName'.
	 * @param completeName Le nom complet de la structure.
	 */
	public void setCompleteName(String completeName) {
		this.completeName = completeName;
	}

	/**
	 * Accesseur en lecture sur l'attribut 'members'.
	 * @return La liste des membres de cette structure.
	 */
	public List<Membre> getMembres() {
		
		List<Membre> membresList = new LinkedList<Membre>();
		
		for (Membre membre : this.membres) {
			if (membre.isAssistantLeader() == false
					&& membre.isLeader() == false
					&& membre.isContact() == false) {
				membresList.add(membre);
			}
		}
		
		return membresList;
	}
	
	private boolean checkReturn(Membre membre) {
		if (membre == null) {
			return true;
		}

		if (membre.getKey() == null) {
			return true;
		}
		
		for (Membre m : this.membres) {
			if (membre.getKey().equals(m.getKey())) {
				return true;
			}
		}
		
		return false;
	}
	
	private void setOtherNonLeader(Membre membre) {
		if (membre.isLeader()) {
			for (Membre m : this.membres) {
				if (m != membre && m.isLeader() == true) {
					m.setLeader(false);
				}
			}
		}
	}
	
	private void setOtherNonContact(Membre membre) {
		if (membre.isContact()) {
			for (Membre m : this.membres) {
				if (m != membre && m.isContact() == true) {
					m.setContact(false);
				}
			}
		}
	}
	
	/**
	 * Ajoute un membre dans cette structure. Si ce membre appartenait à une
	 * autre structure, il n'en fera plus partie. De plus, si il était leader de
	 * son ancienne structure, il ne sera pas leader ici. De même, si il était
	 * le ontact de son ancienne structure, il ne le sera plus ici.
	 * @param membre Le membre à ajouter.
	 */
	public void addMembre(Membre membre) {
		
		if (this.checkReturn(membre)) {
			return;
		}
		
		// TODO: changer l'accès à un membre privé
		// Si le membre a ajouter est associé à une ature structure, on le
		// dissocie de sa structure.
		if (membre.getStructure() != null
				&& !membre.getStructure().equals(this)) {
			membre.getStructure().removeMembre(membre);
		}
		
		this.setOtherNonLeader(membre);
		
		this.setOtherNonContact(membre);
		
		// Sinon on ajoute le membre, et on prévient celui-ci qu'il doit être
		// associé à cette structure.
		this.membres.add(membre);
		
		membre.setStructure(this);
		
	}

	/**
	 * Retire un membre de cette structure.
	 * @param membre Le membre à retirer.
	 */
	public void removeMembre(Membre membre) {
		
		// Si le membre ne fait pas partie de la structure, il n'y a rien à
		// faire.
		if (this.membres.contains(membre) == false) {
			return;
		}
		
		// On retire le membre de la structure.
		this.membres.remove(membre);
		
		// On met la structure du membre à null.
		membre.setStructure(null);
		
		
		if (this.leader != null && this.leader.equals(membre)) {
			this.leader = null;
		}
		
		if (this.contact != null && this.contact.equals(membre)) {
			this.contact = null;
		}
	}
	
	/**
	 * Accesseur en lecture sur l'attribut 'leader'.
	 * @return Le leader de la strucutre, ou null s'il n'y en a pas.
	 */
	public Membre getLeader() {
		
		for (Membre membre : this.membres) {
			if (membre.isLeader()) {
				return membre;
			}
		}
		
		return null;
	}

	/**
	 * Accesseur en lecture sur l'attribut 'contact'.
	 * @return Le contact de cette structure.
	 */
	public Membre getContact() {
		
		for (Membre membre : this.membres) {
			if (membre.isContact()) {
				return membre;
			}
		}
		
		return null;
		
	}
	
	/**
	 * Accesseur en lecture sur l'attribut 'assistantLeaders'.
	 * @return La liste des assistants du leader de la structure (en lecture
	 * seule).
	 */
	public List<Membre> getAssistantLeaders() {
		List<Membre> assistants = new LinkedList<Membre>();
		
		for (Membre membre : this.membres) {
			if (membre.isAssistantLeader() == true) {
				assistants.add(membre);
			}
		}
		
		return Collections.unmodifiableList(assistants);
	}
	
	/**
	 * Accesseur en lecture sur l'attribut 'father'.
	 * @return La structure père de celle-ci.
	 */
	public Structure getFather() {
		return this.father;
	}

	/**
	 * Accesseur en écriture sur l'attribut 'father'. Lorsqu'on définit un père,
	 * cette structure est automatiquement ajouté à la liste des fils du père.
	 * @param father La structure père de celle-ci.
	 */
	public void setFather(Structure father) {
		
		// Si le père a déjà été mis à jour, on ne fait rien.
		if (this.father != null && this.father.equals(father)) {
			return;
		}	
		
		// On interdit les boucles dans la filiation.
		if (this.equals(father) || this.isChild(father)) {
			throw new AnnuaireRuntimeException("Boucle dans la filiation "
					+ "des structures.");
		}
		
		// On prévient l'ancien père qu'on est plus son enfant.
		if (this.father != null) {
			this.father.removeChild(this);
		}
		
		this.father = father;
		
		// On prévient le nouveau père qu'on est son fils.
		if (this.father != null) {
			this.father.addChild(this);
		}
	}

	private boolean isChild(Structure structure) {
		
		boolean res = false;
		
		if (structure == null) {
			return false;
		}
		
		for (Structure s : this.getChilds()) {
			if (s.equals(structure)) {
				return true;
			}
			
			res = res || s.isChild(structure);
		}
		
		return res;
		
	}
	
	/**
	 * Accesseur en lecture sur l'attribut 'childs'.
	 * @return La liste des structures filles de celle-ci (en lecture seule).
	 */
	public List<Structure> getChilds() {
		return Collections.unmodifiableList(this.childs);
	}

	/**
	 * Ajoute une structure fille à celle-ci.
	 * @param structure La structure fille à ajouter.
	 */
	public void addChild(Structure structure) {
		
		// Si la structure est null, on ne fait rien.
		if (structure == null) {
			return;
		}
		
		// Si la structure existe déjà, on ne fait rien.
		if (this.childs.contains(structure)) {
			return;
		}
		
		if (this.equals(structure)) {
			throw new AnnuaireRuntimeException("Boucle dans la filiation "
					+ "des structures.");
		}
		
		// On vérifie qu'on essaie pas de faire de boucle dans la filiation.
		// Si une structure fille veut ajouter un de ses pères en tant que
		// fille, une exception est générée.
		Structure pere = getFather();
		while (pere != null) {
			if (pere.equals(structure)) {
				throw new AnnuaireRuntimeException("Boucle dans la filiation "
						+ "des structures.");
			}
				
			pere = pere.getFather();
		}

		// On ne peut pas s'ajouter soi-même dans les enfants.
		if (structure.equals(this)) {
			return;
		}	

		// Sinon on ajoute la structure, et on prévient celle-ci qu'elle doit
		// être associé à cette structure.
		this.childs.add(structure);
		structure.setFather(this);
	}
	
	/**
	 * Retire une structure fille.
	 * @param child La structure fille à retirer.
	 */
	public void removeChild(Structure child) {
		
		// Si la fille est null, on ne fait rien.
		if (child == null) {
			return;
		}
		
		// Si la fille ne fait pas partie de la liste des filles, on a rien à
		// faire.
		if (this.childs.contains(child) == false) {
			return;
		}
		
		this.childs.remove(child);
		
		child.setFather(null);
	}
	
	/**
	 * La clef de la structure.
	 */
	private String key = null;

	/**
	 * Le nom abrégé (initiales par exemple) de la structure.
	 */
	private String shortName = null;

	/**
	 * Le nom complet de la structure.
	 */
	private String completeName = null;

	/**
	 * La liste des membres de cette structure.
	 */
	private List<Membre> membres = new LinkedList<Membre>();
	
	/**
	 * Le leader de la strucutre.
	 */
	private Membre leader = null;

	/**
	 * Le contact de cette structure.
	 */
	private Membre contact = null;

	/**
	 * La structure père de celle-ci.
	 */
	private Structure father = null;

	/**
	 * La liste des structures filles de celle-ci.
	 */
	private List<Structure> childs = new LinkedList<Structure>();

}
